#!/usr/bin/env python

import pyphon, csv, sys, pickle, os
import hashlib
from optparse import OptionParser

usage = \
"""pyphon_makemodel.py descriptionFile [outputFile]

Read a PyPhon model description file and write the resulting model to disk. If
outputFile is omitted, it defaults to descriptionFile.mod."""

def parseDie(file, lineno, msg):
    print "Failed to read model description file %r (line %d): %s" % (file,
            lineno, msg)
    raise SystemExit

options = {
    'drawConstraints': False,
    'drawEval': False,
    'cacheConstraints': False,
    'cacheDir': None,
}

def openWithExtension(basename, extension, mode):
    suffix = '.'+extension
    if basename.endswith(suffix):
        try:
            return open(basename, mode)
        except IOError:
            pass
    try:
        return open(basename+suffix, mode)
    except IOError:
        pass

    try:
        return open(basename, mode)
    except IOError:
        raise ValueError("Unable to open %r or %r for mode %r" %
                (basename+suffix, basename, mode))

def filenameWithExtension(basename, extension):
    suffix = '.'+extension
    if basename.endswith(suffix):
        return basename
    return basename + suffix

def getCachedConstraint(cname, stateStr):
    if not options['cacheConstraints']:
        return None

    md5 = hashlib.md5()
    md5.update(stateStr)
    hash = md5.digest()
    if options['cacheDir']:
        path = os.sep.join([options['cacheDir'], cname+'.con'])
    else:
        path = cname + '.con'
    try:
        f = open(path, 'r')
        chash, constr = pickle.load(f)
        f.close()
    except:
        return None

    if chash == hash:
        print 'Retrieving cached %r from %s.' % (cname, path)
        return constr
    else:
        return None

def cacheConstraint(cname, stateStr, constr):
    if not options['cacheConstraints']:
        return

    md5 = hashlib.md5()
    md5.update(stateStr)
    hash = md5.digest()
    if options['cacheDir']:
        path = os.sep.join([options['cacheDir'], cname+'.con'])
    else:
        path = cname + '.con'
    print 'Caching constraint/filter %r to %s.' % (cname, path)
    f = open(path, 'w')
    pickle.dump((hash, constr), f)
    f.close()

def main(descriptionFile, outputFile=None):
    ipaFeatures = pyphon.featureMap.ipaFeatures
    ipaDiacriticFeatures = pyphon.featureMap.ipaDiacriticFeatures
    f = openWithExtension(descriptionFile, 'csv', 'rU')
    outputFile = outputFile or '.'.join(f.name.split('.')[:-1])
    print 'Reading model description from %s...' % f.name
    reader = csv.reader(f, skipinitialspace=True)
    inventory = []
    constraintsDefined = [] 
    filtersDefined = [] 
    dotConstraints = []
    con = []

    lineno = 0
    for row in reader:
        lineno += 1
        if (not row) or row[0].strip().startswith('#'):
            continue

        command = row[0].strip()
        if command == 'Option':
            option = row[1].strip()
            value = row[2].strip() in ('True', 'true', '1')
            if option not in options:
                print 'Warning: ignoring unknown option %r' % option
            options[option] = value 

        elif command == 'NewFeatures':
            if inventory:
                parseDie(descriptionFile, lineno,
                    'New features must be listed before the inventory.')

            symbolChar = row[1].strip()
            fea = {}
            for featureSpec in row[2:]:
                featureSpec = featureSpec.strip()
                sign = featureSpec[0]
                feature = featureSpec[1:]
                if sign == '+':
                    fea[feature] = 1
                elif sign == '-':
                    fea[feature] = 0
                else:
                    parseDie(descriptionFile, lineno,
                        "Couldn't parse feature specification %r." %\
                        featureSpec)

            ipaFeatures[symbolChar] = fea
            #print '%s: %r' % (symbolChar, ipaFeatures[symbolChar])
                
        elif command == 'Inventory':
            inventory += [pyphon.Symbol(s.strip()) for s in row[1:] if s]
            print 'Inventory = ', repr(inventory)
            #for s in inventory:
            #    print '%s: %r' % (s, s.features)
            #for s1 in inventory:
            #    for s2 in inventory:
            #        print '%s unifies with %s? %s' % (s1, s2, s1.unifies(s2))

        elif command == 'NewConstraint':
            if con:
                parseDie(descriptionFile, lineno,
                    'New constraints must be defined before CON.')

            name, regex = row[1].strip(), ','.join(row[2:])
            constraintsDefined.append((name, regex))

        elif command in ('NewSurfaceFilter', 'NewStrictFilter'):
            # Difference between surface and strict filter is that strict filter
            # does not have deletion self-loops automatically inserted.
            if con:
                parseDie(descriptionFile, lineno,
                    'New filters must be defined before CON.')

            name, regex = row[1].strip(), ','.join(row[2:])
            filtersDefined.append((name, regex, (command == 'NewStrictFilter')))

        elif command == 'DotConstraint':
            if con:
                parseDie(descriptionFile, lineno,
                    'New constraints must be defined before CON.')
            name, fname = row[1].strip(), row[2].strip()
            dotConstraints.append((name, filenameWithExtension(fname, 'dot')))

        elif command == 'CON':
            con = []
            namesBuilt = []
            for c in row[1:]:
                name = c.strip()
                for (cname, regex) in constraintsDefined:
                    if name == cname:
                        if name in namesBuilt:
                            print 'Notice: pooling violations across multiple instances of '\
                                'constraint %r' % name

                        constr = None
                        stateStr = 'constraint:'+regex
                        if options['cacheConstraints']:
                            constr = getCachedConstraint(name, stateStr)

                        if not constr:
                            split = regex.split('_')
                            if len(split) == 1:
                                target = regex
                                left = None
                                right = None
                            elif len(split) == 3:
                                left, target, right = regex.split('_')
                            else:
                                parseDie(descriptionFile, lineno, 
                                        'The target of %s is ambiguous in %r.' %\
                                                (name, regex))
                            print 'Compiling constraint %s from regex "%s"...' % (name,
                                    regex)
                            constr = pyphon.pycbg.LeftRightContextConstraint(name,
                                    target, left=left, right=right, inventory=inventory)
                            if options['drawConstraints']:
                                constr.dotToFile("%s.dot"%name)
                            if options['cacheConstraints']:
                                cacheConstraint(name, stateStr, constr)

                        con.append(constr)
                        namesBuilt.append(name)
                for (fname, regex, strict) in filtersDefined:
                    if name == fname:
                        if name in namesBuilt:
                            print 'Notice: intersecting multiple filters '\
                                'named %r' % name

                        filter = None
                        stateStr = 'filter:strict=' + str(strict) + ':' +regex
                        if options['cacheConstraints']:
                            filter = getCachedConstraint(name, stateStr)

                        if not filter:
                            print 'Compiling %s filter %s from regex "%s"...' % \
                                    (strict and 'strict' or 'surface', name, regex)
                            filter = pyphon.pycbg.Constraint(name, inventory, regex,
                                    filter=True, strict=strict)
                            if options['drawConstraints']:
                                filter.dotToFile("%s.dot"%name)
                            if options['cacheConstraints']:
                                cacheConstraint(name, stateStr, filter)

                        con.append(filter)
                        namesBuilt.append(name)
                for (cname, fname) in dotConstraints:
                    if name == cname:
                        if name in namesBuilt:
                            print 'Notice: intersecting multiple constraints '\
                                'named %r' % name

                        cfile = open(fname, 'r')
                        constr = None
                        stateStr = 'constraint:' + cfile.read()
                        if options['cacheConstraints']:
                            constr = getCachedConstraint(name, stateStr)

                        if not constr:
                            print 'Compiling constraint %s from DOT file "%s"...' % \
                                    (name, fname)
                            cfile.seek(0)
                            constr = pyphon.pycbg.constraintFromDot(cfile, name,
                                    inventory, ipaFeatures, ipaDiacriticFeatures)
                            if options['cacheConstraints']:
                                cacheConstraint(name, stateStr, constr)

                        cfile.close()
                        con.append(constr)
                        namesBuilt.append(name)


            print 'CON = %s' % (', '.join([str(c) for c in row[1:]]),)
            for c in row[1:]:
                if c.strip() not in namesBuilt:
                    print 'Undefined constraint %r requested in CON.' % c.strip()
                    return

        else:
            parseDie(descriptionFile, lineno, 'Unknown command %r' % command)

    f.close()
    print "Building EVAL..."
    eval = pyphon.pycbg.makeEval(descriptionFile, inventory, con)
    print '%d states.' % len(eval)
    if eval.inventory != inventory:
        s = eval.inventory - set(inventory)
        if s:
            print 'Warning: Constraints refer to symbols not present in model '\
                  'inventory: %s' % (', '.join([str(sym) for sym in s]),)
        s = set(inventory) - eval.inventory
        if s:
            print 'Warning: Some symbols in model inventory are not referenced '\
                  'by any constraints: %s' % (', '.join([str(sym) for sym in s]),)
    eval.inventory = set(inventory)
    if options['drawEval']:
        dotFile = filenameWithExtension(outputFile.replace('.mod', ''), 'dot')
        eval.dotToFile(dotFile)
        print "EVAL graph written to %s" % (dotFile,)

    f = openWithExtension(outputFile, 'mod', 'w')
    print "Saving model to %s..." % f.name
    pickle.dump((eval, ipaFeatures, ipaDiacriticFeatures, {}), f)
    f.close()
    print "Done."


if __name__ == "__main__":
    parser = OptionParser(usage=usage)
    parser.add_option('-V', '--version', action='store_true', dest='version',
            default=False,
            help='Print pyPhon version and quit.')
    cl_options, args = parser.parse_args()

    if cl_options.version:
        print 'pyPhon version %s.' % pyphon.__version__
        raise SystemExit

    if len(args) < 1:
        print usage
    elif len(args) < 2:
        main(args[0])
    elif len(args) < 3:
        main(args[0], args[1])
    else:
        print usage

